<template>
  <div>
    <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
      <div class="infinite-list-phantom" ref="phantom"></div>
      <div class="infinite-list" ref="content">
        <div
        class="infinite-list-item"
        ref="items"
        v-for="(item,index) in visibleData"
        :id="item.index"
        :key="item.index">
          {{item.id + 1}}.{{item.value}}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import faker from 'faker'
export default {
  name: "virtual-list",
  props: {
    // 所有列表数据
    // 单一文件初始化，这里不做props传值
    // listData: {
    //   type: Array,
    //   default: () => []
    // },
  },
  computed: {
    // 可显示的列表项目数量
    visibleCount() {
      return Math.ceil(this.screenHeight / this.estimatedItemSize)
    },
    // 获取可视区域显示的列表数据
    visibleData() {
      // 使用Math.min的意义在于，this.listData长度为100，滚动条拉到最底this.end显示101
      // 实际上可视窗口有6个item分别id为95、96、97、98、99、100，滚动条拉到底为9500每项高为100
      // 所以拉到底时this.start为95，正因为this.listData是数组，索引值95存的内容为96，所以从内容为96的元素开始算this.start
      // 第二个元素正好使用了取最小值，此时this.end为95+6=101，this.listData.length=100
      // 所以this.listData.slice(95,100)因为slice要取到listData的最后一个元素，最后一个元素为99所以写100可以取到数组的最后一个元素
      // 此时visibleCount由6变成5
      // return this.listData.slice(this.start,Math.min(this.end,this.listData.length))
      let start = this.start - this.aboveCount;
      let end = this.end + this.belowCount;
      return this.listData.slice(start, end);
    },
    // 上方缓冲区可视条数
    aboveCount(){
      return Math.min(this.start,this.bufferScale * this.visibleCount)
    },
    // 下方缓冲区可视条数
    belowCount(){
      return Math.min(this.listData.length - this.end,this.bufferScale * this.visibleCount);
    },
  },
  created() {
    // 初始化listData数据
    let data = [];
    for (let id = 0; id < 1000; id++) {
      data.push({
        id,
        value: faker.lorem.sentences() // 长文本
      })
    }
    this.listData = data
    this.initPositions()
  },
  mounted() {
    // 设置可视区域高度 获取当前浏览器的高度
    this.screenHeight = this.$el.clientHeight
    // 初始化起始索引
    this.start = 0
    // 初始化结束索引
    this.end = this.start + this.visibleCount
  },
  updated() {
    this.$nextTick(function() {
      if (!this.$refs.items || !this.$refs.items.length) {
        return
      }
      //获取真实元素大小，修改对应的尺寸缓存
      this.updateItemsSize()
      //更新列表总高度
      let height = this.positions[this.positions.length - 1].bottom
      this.$refs.phantom.style.height = height + "px"
      //更新真实偏移量
      this.setStartOffset()
    })
  },
  data() {
    return {
      name: "virtual-list",
      listData: [], // 所有列表数据
      screenHeight: 0, // 可视区域高度
      start: 0, // 起始索引
      end: 0, // 结束索引
      startOffset: 0, // 偏移量
      estimatedItemSize: 50, // 预估高度
      positions: [], // 储存列表每一项高度以及位置信息
      height: "50vh", //容器高度 100px or 50vh
      bufferScale: 1, // 缓冲区比例
    }
  },
  methods: {
    scrollEvent() {
      // 获取当前滚动条位置
      let scrollTop = this.$refs['list'].scrollTop
      // 设置此时的起始索引
      this.start = this.getStartIndex(scrollTop)
      console.log(this.start)
      // 设置此时的结束索引
      this.end = Math.ceil(this.start + this.visibleCount)
      // 设置此时的偏移量
      // if(this.start >= 1){
      //   this.startOffset = this.positions[this.start - 1].bottom
      // }else{
      //   this.startOffset = 0;
      // }
    },
    /* 
    * @author: XN_Hotaru
    * @name: 初始化列表每项高度等信息 
    */
    initPositions() {
      this.positions = this.listData.map((item,index)=>{
        return {
          index,
          height: this.estimatedItemSize,
          top: index * this.estimatedItemSize,
          bottom: (index + 1) * this.estimatedItemSize
        }
      })
    },
    //获取列表起始索引
    getStartIndex(scrollTop = 0){
      //二分法查找
      return this.binarySearch(this.positions,scrollTop)
    },
    //二分法查找
    binarySearch(list,value){
      let start = 0;
      let end = list.length - 1;
      let tempIndex = null;
      while(start <= end){
        let midIndex = parseInt((start + end)/2);
        let midValue = list[midIndex].bottom;
        if(midValue === value){
          return midIndex + 1;
        }else if(midValue < value){
          start = midIndex + 1;
        }else if(midValue > value){
          if(tempIndex === null || tempIndex > midIndex){
            tempIndex = midIndex;
          }
          end = end - 1;
        }
      }
      return tempIndex;
    },
    //获取列表项的当前尺寸
    updateItemsSize() {
      let nodes = this.$refs.items;
      nodes.forEach(node => {
        let rect = node.getBoundingClientRect()
        let height = rect.height
        let index = +node.id.slice(1)
        let oldHeight = this.positions[index].height
        let dValue = oldHeight - height
        //存在差值
        if (dValue) {
          this.positions[index].bottom = this.positions[index].bottom - dValue
          this.positions[index].height = height
          for (let k = index + 1; k < this.positions.length; k++) {
            this.positions[k].top = this.positions[k - 1].bottom
            this.positions[k].bottom = this.positions[k].bottom - dValue
          }
        }
      })
    },
    //获取当前的偏移量
    setStartOffset() {
      let startOffset = this.start >= 1 ? this.positions[this.start - 1].bottom : 0
      this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`
    },
  }
}
</script>

<style lang="less" scoped>
.infinite-list-container {
  width: 500px;
  height: 500px;
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
}
 
.infinite-list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
  background-color: #555;
}
 
.infinite-list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
  text-align: center;
}
 
.infinite-list-item {
  padding: 10px;
  color: #555;
  box-sizing: border-box;
  border-bottom: 1px solid #999;
  background-color: gold;
}
</style>